Skip to content

feat: per-card AI tool approval pills#1091

Merged
datlechin merged 4 commits into
mainfrom
feat/pr-e-tool-approval-pills
May 7, 2026
Merged

feat: per-card AI tool approval pills#1091
datlechin merged 4 commits into
mainfrom
feat/pr-e-tool-approval-pills

Conversation

@datlechin

Copy link
Copy Markdown
Member

Summary

PR E from docs/refactor/ai-chat-redesign.md. Replaces the global safe-mode NSAlert for AI write/destructive tool calls with inline approval pills on each tool-use card: [Run] [Always for this connection ▾] [Cancel].

Behaviour matrix:

Safe mode Tool kind Result
Silent any auto-run, no pills
Confirm Writes read-only auto-run, no pills
Confirm Writes write/destructive pending pills, await user
Read Only write/destructive denied with reason, no pills

Always for this connection writes the tool name into DatabaseConnection.aiAlwaysAllowedTools (new persisted Set). Wired through all five persistence layers: DatabaseConnection.Codable, StoredConnection on-disk, duplicateConnection, SyncRecordMapper for CloudKit, and the connection form UI (extension to existing AI Rules pane).

Cancel ends only that tool call; the assistant gets an error result and continues. The streaming task itself stays alive.

AIChatViewModel.cancelStream now also drains pending approvals via ToolApprovalCenter.cancelAll().

Architecture

Three new files plus an extension:

  • ToolApprovalCenter (MainActor): awaitDecision(toolUseId) registers a CheckedContinuation<ToolApprovalDecision, Never>; resolve(toolUseId, decision) resumes it. The view buttons call resolve; the viewmodel calls awaitDecision.
  • ToolApprovalState enum on ToolUseBlock: approved, pending, denied(reason), cancelled. Codable-forward-compatible (decodeIfPresent ?? .approved).
  • ToolApprovalActionsRow SwiftUI view: the three buttons. .borderedProminent Run, .borderlessButton Always menu, .bordered Cancel. Native HIG, no custom chrome.
  • AIChatViewModel+ToolApproval: extension carrying the new resolveAndAwaitApprovals, synthesizeResults, and approval-state mutation helpers (extracted because the main file was at the SwiftLint type body length warning threshold).

AIChatViewModel.runStream was restructured. Previously: executeToolUses → appendToolRoundtrip(toolUseBlocks + results). Now: assemble → resolveAndAwaitApprovals (appends pending blocks, awaits decisions) → executeToolUses (only approved) → synthesizeResults → completeToolRoundtrip. Tool cards now appear before execution, not after.

ExecuteQueryChatTool and ConfirmDestructiveOperationChatTool no longer call MCPAuthPolicy.checkSafeModeDialog. The viewmodel layer covers approval and Read-Only blocking. The MCP server tools (separate files) keep checkSafeModeDialog since external clients have no card UI.

Out of scope, observed in passing

The PR G aiRules field on DatabaseConnection is currently wired into Codable but absent from StoredConnection and SyncRecordMapper. It silently drops on save/reload and does not sync to iCloud. This is unrelated to PR E and should land as a separate hotfix.

Files modified

  • Core/AI/Chat/ChatTurn.swiftToolApprovalState + ToolUseBlock field
  • Core/AI/Chat/ToolApprovalCenter.swift (new) — continuation registry
  • Core/AI/Chat/ChatToolRegistry.swiftrequiresApproval(toolName:)
  • Core/AI/Chat/Tools/ExecuteQueryChatTool.swift — drop checkSafeModeDialog
  • Core/AI/Chat/Tools/ConfirmDestructiveOperationChatTool.swift — drop checkSafeModeDialog
  • Models/Connection/DatabaseConnection.swiftaiAlwaysAllowedTools: Set<String>
  • Core/Storage/ConnectionStorage.swiftStoredConnection.aiAlwaysAllowedTools round-trip
  • Core/Sync/SyncRecordMapper.swift — CloudKit field
  • ViewModels/AIChatViewModel.swift — restructured runStream, cancelStream cancels pending approvals
  • ViewModels/AIChatViewModel+ToolApproval.swift (new) — approval lifecycle methods
  • Views/AIChat/AIChatToolUseBlockView.swift — pending state label, render ToolApprovalActionsRow when pending
  • Views/AIChat/ToolApprovalActionsRow.swift (new) — three-button HIG row
  • CHANGELOG.md — Added entry under [Unreleased]

Test plan

  • Build the project. swiftlint --strict clean on all touched files.
  • Connect to a writable database, set safe mode to Confirm Writes. Switch chat mode to Edit or Agent.
  • Ask the AI to update users set is_active = true. The tool card appears with Run / Always / Cancel pills. No NSAlert modal.
  • Click Run. Query executes. Card swaps to running state, then result block appears.
  • Ask again. Click Always for this connection. Query executes. Inspect connection settings: execute_query is whitelisted.
  • Ask again. Auto-runs without pills.
  • Quit and relaunch the app. Repeat: still auto-runs (persisted across launches).
  • Switch safe mode to Read Only. Ask the AI to delete from users. Tool card shows Blocked, no pills.
  • Switch safe mode to Silent. Ask write. Auto-runs, no pills.
  • Mid-stream press the stop button. Any pending tool calls resolve to cancelled, assistant continuation aborts cleanly.
  • Read-only tools (list_tables, describe_table) never show pills regardless of safe mode.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin datlechin merged commit d97dd3e into main May 7, 2026
2 checks passed
@datlechin datlechin deleted the feat/pr-e-tool-approval-pills branch May 7, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant